#node.js + express4 写一个自己的博客网站[1]
- 0.前言
- 1.需求
- 2.技术选型
- 3.正文,动手
- 3.1 Hello, Express
- 3.2 Express 中的 Routing
- 3.3 认识一下 Middleware
0. 前言
这个系列的文章可以看作个人项目blog-node的记录,同时也算一个nodejs 进阶版hello world范例。希望可以为像我一样想快速入门nodejs的大家一点小小的方向。自己也是刚刚开始进入nodejs世界,因此文中或有一些初级的,入门的,甚至可能是错误的知识与观点,还请不吝指正。
因为是“进阶版”hello world,因此本文会跳过一些我认为即使对我这样的新手来说也非常基础的地方而略过不提,比如如何搭建环境,如何使用npm等等,网上关于这些的文章已经足够多了。
所以,如果你对Nodejs还一无所知,强烈推荐这本小书:Node入门,篇幅不长但讲解详细而又浅显易懂。相信是个极好的学习开端。
最后,文章内的所有代码几乎都可以在本项目中找到,或许偶有为了讲解方便而略作修改之处。欢迎任何的意见和批评~
1. 需求
不知道大家有没有看过像黑客一样写博客或是类似的介绍利用Jekyll来进行博客写作的文章?本着不折腾会死不造轮子不幸福的码农精神。今天来试试从头搭建一个类jekyll的静态网站。这个网站应该至少能满足以下需求:
- 自动解析markdown文件成html
- 根据一定的文件名规则生成对应的url
- 如果能根据文章的Tag/Category或者其他任何自定义的属性自动分类就更好了
- ...暂时还没想到~
如果您曾尝试过jekyll或类似系统,或许就会发现,以上列出的基本就是一个minimal jekyll website。 那么, 接下来分析一下如何实现。
2. 技术选型
不消说我们要开发的是一个网站。基础技术也已确定NodeJs不做他选(题目就是这个嘛)。
时下火热的 MEAN Web-Dev Stack必定值得一试。不过根据初期需求来看,静态博客暂时不需要引入数据库或复杂的页面交互结构,为了快速实现一个原型,目前仅需 M.E.A.N 中的 E[xpress] + N[odejs] 就很足够了。
关于Express或是Nodejs的基础知识,例如开发环境搭建什么的,这里就不再重复。我自己则是在Mac/Ubuntu/Windows下同步进行后文中的所有工作,基本就是Nodejs环境 + SublimeText/Vim + Git(跨平台的软件就是一个赞)通吃所有。
直接进入主题。
3. 动手
撩起袖子大干一场之前,先给之后的工作做一个简单的分解和排序:细化一下上文中的需求列表,我们的第一个目标就是把文章显示到页面上!
...确实简单了点儿,不过由浅入深嘛。借此机会了解一下Nodejs与Express的基本知识。
3-1 Hello, Express
新建项目
|--myblog-node
| |--app.js
| |--package.json
package.json为我们提供了一个统一控制包依赖关系以及程序自描述的入口,唔,你大概可以把他想像成C#项目中sln/csproj文件之类的东西(=。=暴露了,其实我是个.NET农民)。内容如下:
{
"name": "my-blog",
"version": "0.0.1",
"author": "NarK",
"dependencies":{
"express": "4.x"
}
}
内容很清楚不解释,但在这里可以看到package.json所能做的远不止这些,我们以后再谈。保存后运行npm install
等待依赖包安装完成。
app.js则是我们今后的主程序文件。
//app.js
var express = require('express');
var app = express();
app.get('/', function(req, res) {
res.send('hello world');
});
app.listen(3000);
以上代码直接从Express官网API Doc复制,顺便一说,本项目采用目前最新的4.x版本。
这样实现了一个express下的最简server,打开浏览器访问 http://localhost:3000 即可见效。
OK,那么本文到此为止。
...
...
...
...开个玩笑。从上述代码可见,Express中的路由控制不需要我们再去手动解析request,然后苦哈哈的写上一句又一句if (method === 'POST' && path === '/home')
之类的判断。取而代之的是相当形象而易写的 app.verb(path, callback)
方法。verb 可以是 post,get 等等。
而在请求处理方法中,res.send(content)
也提供了一个简单的响应方式,如果不显示指定Content-Type
,express会根据send方法的参数自动推定响应类型,列个表格出来:
Data Type Content-Type
Buffer application/octet-stream
String text/html
Array/Object Json representation
Number return a respond text: 200 <=> "OK" for example
参见res.send()。
于是很明显,接下来我们无非是把文章内容从文件中读取出来,再res.send()一下就ok。
查阅一下nodejs中与文件系统相关的api,加上读取文件的代码后,app.js的内容如下:
//app.js
var express = require('express');
var fs = require('fs');
var app = express();
app.get('/', function(req, res) {
fs.readFile('./blogs/test.md', function (err, data) {
if (err) res.send(err);
res.send(data);
});
});
app.listen(3000);
当然了,在运行前记得先创建blogs文件夹与test.md文件(随手写点内容咯,不然看不到效果)。
编码完工~切换到终端,输入 nodemon app.js
,我们来看一下是否成功读取了本博客的第一篇文章。
哦对了,强烈推荐一个小工具 nodemon 。一句话简介:全局安装了nodemon后,我们可以通过nodemon xxx.js的方式启动nodejs程序,而在此方式下启动的程序会自动侦测与本程序相关的文件,随时自动重启进程以反映最新的变化。实乃nodejs开发debug过程中必备利器!
言归正传,我志得意满的打开chrome浏览器访问localhost:3000,意料中的文字却没有出现,反而弹出了一个文件下载询问框。shit!谁告诉我send()方法会自动推定Content-Type的!?打开网络侦测一看,果不其然,返回的Content-Type是 application/octet-stream
。(经测试,在FireFox中同样提示下载文件,有点搞笑的是,IE11倒是老老实实的直接在页面显示了文件内容...IE大哥你怎么老跟别人不一样啊...)
Well~我重新翻阅了nodejs的文档,对于fs.readFile(path, callback (err, data))
的解释最后有一句话:
If no encoding is specified, then the raw buffer is returned.
得得~这就是看文档不仔细的后果。查阅上文表格可见,buffer
对应的content-type
确实是application/octet-stream
来着,修改代码:
fs.readFile('./blogs/test.md', 'utf-8', function (err, data) {
if (err) res.send(err);
res.send(data);
});
刷新页面(nodemon已经在我们保存代码文件时自动重启: [nodemon] restarting due to changes...
),当当~成功显示!啥?你说这个页面一点儿都不好看?不要在意这些细节...我们根本就是原样输出了markdown文件内容,连html都没转换,当然好看不了,稍安勿躁~
以上,我们建立了一个最简单的Hello, Express项目,介绍了package.json,express中的简单路由控制,res.send()
,fs.readFile()
,以此完成了一个读取本地文件显示到页面的功能。接下来,我们会在此基础上,完成一个自动检测所有指定格式的blog文件,并一一映射到对应URL的功能。
3-2 Express中的Routing
很明显,我们的网站不会只有一篇博文,网站的首页也不应该直挺挺的就打印出一篇文章来。所以下一步是构思一下网站的路由结构,
|-- Home
| |--Blog
| | |--blogA
| | |--blogB
| | |--blog...
| |--xxx
那么,首页自然应该显示一个文章列表,点击文章后导向一个 host/blog/xxxxx
的url,显示对应文章的内容。相当常见的组织方式~
有了上节的经验,我们很快就能写出类似于这样的代码:
//...
app.get('/blog/blogA', function (req, res) {
fs.readFile('./blogs/blogA.md', 'utf-8', function (err, data) {
if (err) res.send(err);
res.send(data);
});
});
以此类推,有两百篇文章就写上两百个这样的路由方法=。=
当然不是这样...于是,为了可以批量读取到所有指定目录下的markdown文件,我们势必要给它订立一个标准的命名格式,比如:*.md,只要是markdown文件的我都认;不过或许我们可以把标准订的更严格一些。
比如:模仿jekyll的默认命名格式: yyyy-MM-dd-blog-title.md
这样一来我们甚至可以依靠文件名就简单的为他们做一个按日期分组。或者直接就体现在url上,比如 host/blog/2014/04/30/express-plus-nodejs-making-my-own-blog
。
于是,我们的路由方法可以从两百个变成一个 ;)
app.get('/blog/:year/:month/:day/:title', function (req, res) {
var fileName =
'./blogs/' +
req.params.year + '-' +
req.params.month + '-' +
req.params.day + '-' +
req.params.title + '.md';
fs.readFile(fileName, 'utf-8', function (err, data) {
if (err) {
res.send(err);
}
res.send(data);
});
});
这里我们认识了Express又一个十分方便的路由功能:唔..我不知道它叫啥,姑且称为命名请求参数?总之,在 app.verb(url, callback (req, res))
中的url上,我们可以使用 :argname
的形式为url的部分字符命名,然后通过req.params.argname
获取,如上所示。
确保建立了正确的文件夹与按规则命名的markdown文件后,启动程序访问一下看看咯~
这样,利用 yyyy-MM-dd-blog-title.md
的命名规则,配合Express中的命名请求参数:形如 /blog/:year/:month/:day/:title
这样的url来解析指向相应的任意md文件。
接下来要做的,就是从本地已有的文件反向得到该文件的url,以此生成一个文章列表供用户点击。
function getBlogList(blogDir) {
fs.readdir(blogDir, function (err, files) {
var blogList = [];
if (files && files.length) {
files.forEach(function (filename) {
//split file name and generate url...
//...
//create a blogItem { title: blogTitle, url: blogUrl }
blogList.push(blogItem);
});
}
return blogList;
});
}
限于篇幅,我去掉了关于文件名格式的正则验证,对文件名的解析生成url的过程(只是简单的截取字符串而已)等等细节的部分。总之,经过以上繁琐的字符串处理,我们最终得到了一个形如
{[
{ title: 'blogA', url: '/blog/2014/04/01/blogA'},
{ title: 'blogB', url: '/blog/2014/05/08/blogB'},
...
]}
这样的对象。
于是我们就可以在首页显示所有文章的链接了:
app.get('/', function (req, res) {
var html = '';
var blogList = getBlogList('./blog');
if (blogList && blogList.length) {
blogList.forEach(function (blog) {
html += '<a href="'+ blog.url +'">' + blog.title + '</a><br/>';
});
res.send(html);
} else {
res.send('No Blogs Found.');
}
});
大功告成~现在用户可以通过首页上的列表访问任意一篇存在于blog文件夹下且命名符合规则的文章了。
3-3 认识一下Middleware
终于来到了Express中,准确的说是Connect(Express的一个基础组件,当然也可以作为一个单独的框架使用,主要负责了中间件机制的实现)中激动人心的中间件部分。
有关中间件的解释,参见 A short guide to Connect Middleware。 我在这里就不多卖弄自己的浅薄见解了,简单来说,可以把中间件机制想像成一个层层过滤的污水处理系统(=。=抱歉,但是这是我第一个想到的比喻...)。request
经过一个又一个的中间件,有的结束了处理response
到了客户端,有的则继续流入下一个中间件。
其实在我们之前的代码中,已经在无意中使用了这一特性:
...
app.get('/', function (req, res){
//index page
});
app.get('/blog/:year/:month/:day/:title', function (req, res) {
//blog page
});
...
app.verb()
其在本质上就是一个带有高级路由功能的中间件,request自上而下首先来到app.get('/')
,判断url是否匹配,匹配则进入处理方法,否则继续"流向"下一个中间件。
那么,如果我们希望加上一个自定义的404 Not Found页面的话,应该如何利用中间件的这一特性呢?
简单,只需要把它放在“过滤网”的最底层就好了:
...
app.get('/', ...);
app.get('/blog/', ...);
app.get('/wiki/', ...);
...
app.get('*', function (req, res) {
res.send(404, "Oops! We didn't find it");
});
可以被解析匹配的请求路径在各自对应的中间件中被一一处理并返回了结果,剩下所有能够到达最底层的请求则是无法被已有路由解析的,于是返回404。
简洁而自然的处理方式!
而有的时候,一个请求在经过首个匹配的中间件处理后,我们可能还希望它继续行进到下一个匹配的中间件中去,在处理方法中显式使用next()
即可。
app.use(function (req, res, next) {
console.log(req.method + ',' + req.url);
next();
});
app.get('/', ...);
...
关于中间件的介绍就到此为止,更多的知识及应用会在后面逐一提及。我们还是把重心放回到当前的项目中来。
如今我们的网站已经可以将用户从主页导向至任意一篇文章,接下来就该把markdown文件正式转换为html的格式以供读者阅读了...
第一篇结束,多谢观看。
请!
看!
下!
集!
有没有点黑猫警长的范儿~ ;)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。